/*
 * Copyright (c) 2017-present, CV4J Contributors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.cv4j.core.filters.face;

import com.cv4j.core.binary.Erode;
import com.cv4j.core.datamodel.ByteProcessor;
import com.cv4j.core.datamodel.ImageProcessor;
import com.cv4j.core.datamodel.IntIntegralImage;
import com.cv4j.core.datamodel.Size;
import com.cv4j.core.filters.CommonFilter;
import com.cv4j.core.filters.FastEPFilter;
import com.cv4j.core.filters.GradientFilter;

import java.util.Arrays;
import java.util.Optional;

/**
 * 美丽的皮肤过滤器
 *
 * @author dev
 * @date 2021/07/22
 */
public class BeautySkinFilter implements CommonFilter {
    private float sigma;
    private float ksize;

    /**
     * 美丽的皮肤过滤器
     *
     * @param sigma σ
     * @param ksize ksize
     */
    public BeautySkinFilter(float sigma, float ksize) {
        this.sigma = sigma;
        this.ksize = ksize;
    }

    /**
     * 美丽的皮肤过滤器
     */
    public BeautySkinFilter() {
        this(20, 10);
    }

    /**
     * 过滤器
     *
     * @param src src
     * @return {@link ImageProcessor}
     */
    @Override
    public Optional<ImageProcessor> filter(ImageProcessor src) {
        int width = src.getWidth();
        int height = src.getHeight();
        int length = width * height;
        byte[] red = new byte[length];
        byte[] greed = new byte[length];
        byte[] bule = new byte[length];
        byte[] mask = putBytes(src, length, red, greed, bule);
        int r1;
        int g1;
        int b1;
        Erode erode = new Erode();
        erode.process(new ByteProcessor(mask, width, height), new Size(5));
        for (int i = 0; i < mask.length; i++) {
            int cByte = mask[i] & 0xff;
            if (cByte > 0) {
                src.toByte(0)[i] = red[i];
                src.toByte(1)[i] = greed[i];
                src.toByte(2)[i] = bule[i];
            }
        }

        int cByte = 0;
        for (int i = 0; i < red.length; i++) {
            r1 = red[i] & 0xff;
            g1 = greed[i] & 0xff;
            b1 = bule[i] & 0xff;
            cByte = (int) (0.299 * r1 + 0.587 * g1 + 0.114 * b1);
            mask[i] = (byte) cByte;
        }
        GradientFilter gradientFilter = new GradientFilter();
        int[] gradient = gradientFilter.gradient(new ByteProcessor(mask, width, height));
        Arrays.fill(mask, (byte) 0);
        for (int i = 0; i < red.length; i++) {
            if (gradient[i] > 35) {
                mask[i] = (byte) 255;
            }
        }
        gradient = null;
        // 遮罩层模糊
        byte[] blurmask = getBlurMaskBytes(width, height, mask);
        // alpha blend
        mask = null;
        alphaBlend(blurmask, red, greed, bule, src);
        red = null;
        greed = null;
        bule = null;
        blurmask = null;
        return Optional.ofNullable(src);
    }

    private byte[] putBytes(ImageProcessor src, int length, byte[] red, byte[] greed, byte[] bule) {
        System.arraycopy(src.toByte(0), 0, red, 0, red.length);
        System.arraycopy(src.toByte(1), 0, greed, 0, greed.length);
        System.arraycopy(src.toByte(2), 0, bule, 0, bule.length);
        fastFilterSetting(src);
        ISkinDetection skinDetector = new DefaultSkinDetection();
        byte[] mask = new byte[length];
        Arrays.fill(mask, (byte) 0);
        int r1 = 0;
        int g1 = 0;
        int b1 = 0;
        for (int i = 0; i < red.length; i++) {
            r1 = red[i] & 0xff;
            g1 = greed[i] & 0xff;
            b1 = bule[i] & 0xff;
            if (!skinDetector.isSkin(r1, g1, b1)) {
                mask[i] = (byte) 255;
            }
        }
        return mask;
    }

    private byte[] getBlurMaskBytes(int width, int height, byte[] mask) {
        IntIntegralImage ii = new IntIntegralImage();
        ii.setImage(mask);
        ii.calculate(width, height);
        byte[] blurmask = new byte[mask.length];
        int x2 = 0;
        int y2 = 0;
        int x1 = 0;
        int y1 = 0;
        int cx = 0;
        int cy = 0;
        for (int row = 0; row < height - 1; row++) {
            y2 = (row + 1) > height ? height : (row + 1);
            y1 = (row - 3) < 0 ? 0 : (row - 3);
            for (int col = 0; col < width - 1; col++) {
                x2 = (col + 1) > width ? width : (col + 1);
                x1 = (col - 3) < 0 ? 0 : (col - 3);
                cx = (col - 1) < 0 ? 0 : col - 1;
                cy = (row - 1) < 0 ? 0 : row - 1;
                int sr = ii.getBlockSum(x1, y1, x2, y2);
                blurmask[cx * width + cy] = (byte) (sr / 25);
            }
        }
        ii = null;
        return blurmask;
    }

    private void alphaBlend(byte[] blurmask, byte[] red, byte[] greed, byte[] bule, ImageProcessor src) {
        float w1 = 0.0f;
        int wc = 0;
        int r1 = 0;
        int g1 = 0;
        int b1 = 0;
        for (int i = 0; i < blurmask.length; i++) {
            wc = blurmask[i] & 0xff;
            w1 = wc / 255.0f;
            r1 = (int) ((red[i] & 0xff) * w1 + (src.toByte(0)[i] & 0xff) * (1.0f - w1));
            g1 = (int) ((greed[i] & 0xff) * w1 + (src.toByte(1)[i] & 0xff) * (1.0f - w1));
            b1 = (int) ((bule[i] & 0xff) * w1 + (src.toByte(2)[i] & 0xff) * (1.0f - w1));

            src.toByte(0)[i] = (byte) r1;
            src.toByte(1)[i] = (byte) g1;
            src.toByte(2)[i] = (byte) b1;
        }
    }

    private void fastFilterSetting(ImageProcessor src) {
        FastEPFilter epFilter = new FastEPFilter();
        epFilter.setSigma(sigma);
        epFilter.setWinsize((int) ksize);
        epFilter.filter(src);
    }
}
